package org.windowkit.toolkit.databinding;

import java.awt.Component;
import java.awt.Container;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.swing.JTextField;

import org.ddevil.data.Data;

/**
 * A <code>Binder</code> is the object that actually does the data binding.
 * Takes a {@link DataBoundPanel} and binds it with a given {@link Data} object.
 *
 * @author Rich O'Connell
 */
public class Binder {

	private static final Map<Class<?>, ComponentBinder<? extends Component>> defaultHandlers;
	static {
		defaultHandlers = new HashMap<Class<?>, ComponentBinder<? extends Component>>();
		defaultHandlers.put(JTextField.class, new TextFieldBinder());
	}

	private final Map<Class<?>, ComponentBinder<? extends Component>> customHandlersByClass
		= new HashMap<Class<?>, ComponentBinder<? extends Component>>();
	private final Map<String, ComponentBinder<? extends Component>> customHandlersById
		= new HashMap<String, ComponentBinder<? extends Component>>();


	public void bind(DataBoundPanel panel, Data data) {
		List<DataBoundPanel> boundPanels = findAllDataBoundPanels(panel);

		doBind(panel, data);
		for (DataBoundPanel dbp : boundPanels) {
			doBind(dbp, data);
		}
	}

	protected void doBind(DataBoundPanel panel, Data data) {
		Map<String, Component> comps = panel.getComps();

		for (Entry<String, Component> entry : comps.entrySet()) {
			String id = entry.getKey();
			Component component = entry.getValue();

			// TODO fix this generic issue, if possible
			ComponentBinder<Component> b = (ComponentBinder<Component>)findBinderForComponent(component, id);
			b.bind(component, data.getValue(id));
		}
	}

	/**
	 * Recursively find all {@link DataBoundPanel}s contained in the given Container.
	 * @param parent
	 * @return
	 */
	protected List<DataBoundPanel> findAllDataBoundPanels(Container parent) {
		List<DataBoundPanel> rv = new ArrayList<DataBoundPanel>();
		for (Component c : parent.getComponents()) {
			// if this is a container, then search its children as well
			if (Container.class.isAssignableFrom(c.getClass())) {
				rv.addAll(findAllDataBoundPanels((Container)c));
			}
			// if the component is a DataBoundPanel then we want it
			if (DataBoundPanel.class.isAssignableFrom(c.getClass())) {
				rv.add((DataBoundPanel)c);
			}
		}
		return rv;
	}

	/**
	 * Find the appropriate binder for the given component with the given id.
	 * First the binders registered by id are checked.
	 * Second the binders registered by class.
	 * Lastly the default binders are checked.
	 * This class will never return <code>null</code>, it will throw a run time if no suitable binder
	 * is found.
	 */
	protected ComponentBinder<? extends Component> findBinderForComponent(Component c, String id) {
		// first try by id
		if (customHandlersById.containsKey(id)) {
			return customHandlersById.get(id);
		}
		ComponentBinder<? extends Component> b = findBinderByClass(c.getClass(), customHandlersByClass);
		if (b != null) {
			return b;
		}
		b = findBinderByClass(c.getClass(), defaultHandlers);
		if (b == null) {
			throw new RuntimeException("Unable to find binder for component of class: "
					+ c.getClass() + ", with id: " + id);
		}
		return b;
	}

	/**
	 * Search for the map for a binder that is a match for the given class.
	 * Will search the entire map looking for a super class.
	 */
	protected ComponentBinder<? extends Component> findBinderByClass(Class<?> clazz,
			Map<Class<?>, ComponentBinder<? extends Component>> map) {
		// if the map contains the exact class, then return it
		if (map.containsKey(clazz)) {
			return map.get(clazz);
		}
		//else, check for a registered super class
		for (Entry<Class<?>, ComponentBinder<? extends Component>> entry : map.entrySet()) {
			if (entry.getKey().isAssignableFrom(clazz)) {
				return entry.getValue();
			}
		}
		return null;
	}

	/**
	 * Register a custom binder for the given class.
	 * @param clazz the Class
	 * @param binder the binder
	 * @return this for method chaining
	 */
	public Binder registerBinder(Class<?> clazz, ComponentBinder<? extends Component> binder) {
		customHandlersByClass.put(clazz, binder);
		return this;
	}

	/**
	 * Register a custom binder for the given id.  This binder will take precedence over a binder
	 * registered by class.
	 * @param id the identifier
	 * @param binder the binder
	 * @return this for method chaining
	 */
	public Binder registerBinder(String id, ComponentBinder<? extends Component> binder) {
		customHandlersById.put(id, binder);
		return this;
	}

}
